package hu.mapro.model.roo.fields;

import hu.mapro.model.domain.BeanInformation;
import hu.mapro.model.generator.Annotater;
import hu.mapro.model.generator.AutoBeanGenerator;
import hu.mapro.model.generator.BeanMetadataGenerator;
import hu.mapro.model.generator.ClassCustomizer;
import hu.mapro.model.generator.MetaFactory;
import hu.mapro.model.generator.MetaFactory.FieldInfo;
import hu.mapro.model.generator.MetaFactory.TypeInfo;
import hu.mapro.model.generator.PropertyInformation;
import hu.mapro.model.generator.PropertyInformationBean;
import hu.mapro.model.meta.MetaUtils;
import hu.mapro.model.roo.MaproModelJavaType;

import java.io.ByteArrayOutputStream;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.osgi.service.component.ComponentContext;
import org.springframework.roo.addon.gwt.GwtFileManager;
import org.springframework.roo.classpath.PhysicalTypeCategory;
import org.springframework.roo.classpath.PhysicalTypeIdentifier;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.details.BeanInfoUtils;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.MemberFindingUtils;
import org.springframework.roo.classpath.details.MethodMetadata;
import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType;
import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadata;
import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue;
import org.springframework.roo.classpath.details.annotations.ClassAttributeValue;
import org.springframework.roo.classpath.details.annotations.StringAttributeValue;
import org.springframework.roo.classpath.scanner.MemberDetails;
import org.springframework.roo.classpath.scanner.MemberDetailsScanner;
import org.springframework.roo.metadata.AbstractHashCodeTrackingMetadataNotifier;
import org.springframework.roo.metadata.MetadataIdentificationUtils;
import org.springframework.roo.metadata.MetadataItem;
import org.springframework.roo.model.DataType;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.project.maven.Pom;

import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.primitives.Primitives;
import com.sun.codemodel.JAnnotatable;
import com.sun.codemodel.JAnnotationArrayMember;
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;
import com.sun.codemodel.JTypeVar;

@Component(immediate = true)
@Service
public class MaproModelFieldsMetadataProviderImpl extends
        AbstractHashCodeTrackingMetadataNotifier implements
        MaproModelFieldsMetadataProvider {

    @Reference protected GwtFileManager gwtFileManager;
    @Reference protected ProjectOperations projectOperations;
    @Reference protected TypeLocationService typeLocationService;
    @Reference private MemberDetailsScanner memberDetailsScanner;
    

    protected void activate(final ComponentContext context) {
        metadataDependencyRegistry.registerDependency(
                PhysicalTypeIdentifier.getMetadataIdentiferType(),
                getProvidesType());
    }

    protected void deactivate(final ComponentContext context) {
        metadataDependencyRegistry.deregisterDependency(
                PhysicalTypeIdentifier.getMetadataIdentiferType(),
                getProvidesType());
    }

    public MetadataItem get(final String metadataIdentificationString) {
        final ClassOrInterfaceTypeDetails fieldsClass = getGovernor(metadataIdentificationString);
        if (fieldsClass == null) {
            return null;
        }

        String fileContent = null;
        
//        {
//	        final AnnotationMetadata mirrorAnnotation = MemberFindingUtils
//	                .getAnnotationOfType(fieldsClass.getAnnotations(),
//	                        MaproModelJavaType.ROO_MAPRO_FIELDS);
//	        if (mirrorAnnotation != null) {
//	        	
//				fileContent = generateFields(
//	        			fieldsClass.getName(), 
//	        			mirrorAnnotation, 
//	        			fieldsClass.getAnnotations()
//				);
//	            	
//	        }
//        }
//     
//        {
//        	final AnnotationMetadata mirrorAnnotation = MemberFindingUtils
//        			.getAnnotationOfType(fieldsClass.getAnnotations(),
//        					MaproModelJavaType.ROO_MAPRO_INTERFACES);
//        	if (mirrorAnnotation != null) {
//        		
//        		fileContent = generateInterfaces(
//        				fieldsClass.getName(), 
//        				mirrorAnnotation, 
//        				fieldsClass.getAnnotations()
//        				);
//        		
//        	}
//        }
//        
//        {
//        	final AnnotationMetadata mirrorAnnotation = MemberFindingUtils
//        			.getAnnotationOfType(fieldsClass.getAnnotations(),
//        					MaproModelJavaType.ROO_MAPRO_PROXIES);
//        	if (mirrorAnnotation != null) {
//        		
//        		fileContent = generateProxies(
//        				fieldsClass.getName(), 
//        				mirrorAnnotation, 
//        				fieldsClass.getAnnotations()
//        				);
//        		
//        	}
//        }
        
        {
        	final AnnotationMetadata mirrorAnnotation = MemberFindingUtils
        			.getAnnotationOfType(fieldsClass.getAnnotations(),
        					MaproModelJavaType.ROO_MAPRO_AUTOBEANS);
        	if (mirrorAnnotation != null) {
        		
        		fileContent = generateAutoBeans(
        				fieldsClass.getName(), 
        				mirrorAnnotation, 
        				fieldsClass.getAnnotations()
        				);
        		
        	}
        }
        
        if (fileContent!=null) {
            final String destFile = typeLocationService
                    .getPhysicalTypeCanonicalPath(fieldsClass.getDeclaredByMetadataId());
            
        	gwtFileManager.write(destFile,
            			fileContent);
            	
        	final MaproModelFieldsMetadata metadata = new MaproModelFieldsMetadata(
        			metadataIdentificationString, fileContent);
        	notifyIfRequired(metadata);
            	
        	return metadata;
        }
        
        return null;
        
    }

    ClassOrInterfaceTypeDetails getGovernor(
            final String metadataIdentificationString) {
        final JavaType governorTypeName = MaproModelFieldsMetadata
                .getJavaType(metadataIdentificationString);
        final LogicalPath governorTypePath = MaproModelFieldsMetadata
                .getPath(metadataIdentificationString);

        final String physicalTypeId = PhysicalTypeIdentifier.createIdentifier(
                governorTypeName, governorTypePath);
        return typeLocationService.getTypeDetails(physicalTypeId);
    }

    public String getProvidesType() {
        return MaproModelFieldsMetadata.getMetadataIdentifierType();
    }

    public void notify(final String upstreamDependency,
            String downstreamDependency) {
    	
    	
        if (MetadataIdentificationUtils
                .isIdentifyingClass(downstreamDependency)) {
            Validate.isTrue(
                    MetadataIdentificationUtils.getMetadataClass(
                            upstreamDependency).equals(
                            MetadataIdentificationUtils
                                    .getMetadataClass(PhysicalTypeIdentifier
                                            .getMetadataIdentiferType())),
                    "Expected class-level notifications only for PhysicalTypeIdentifier (not '"
                            + upstreamDependency + "')");

            final ClassOrInterfaceTypeDetails cid = typeLocationService
                    .getTypeDetails(upstreamDependency);
            if (cid == null) {
                return;
            }
            if (MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(),
            		MaproModelJavaType.ROO_MAPRO_FIELDS) != null ) {
                // A physical Java type has changed, and determine what the
                // corresponding local metadata identification string would have
                // been
                final JavaType typeName = PhysicalTypeIdentifier
                        .getJavaType(upstreamDependency);
                final LogicalPath typePath = PhysicalTypeIdentifier
                        .getPath(upstreamDependency);
                downstreamDependency = MaproModelFieldsMetadata.createIdentifier(
                        typeName, typePath);
            	doEvictAndGet(upstreamDependency,downstreamDependency);
            } else if (MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(),
            		MaproModelJavaType.ROO_MAPRO_INTERFACES) != null ) {
                // A physical Java type has changed, and determine what the
                // corresponding local metadata identification string would have
                // been
                final JavaType typeName = PhysicalTypeIdentifier
                        .getJavaType(upstreamDependency);
                final LogicalPath typePath = PhysicalTypeIdentifier
                        .getPath(upstreamDependency);
                downstreamDependency = MaproModelFieldsMetadata.createIdentifier(
                        typeName, typePath);
            	doEvictAndGet(upstreamDependency,downstreamDependency);
            } else if (MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(),
            		MaproModelJavaType.ROO_MAPRO_PROXIES) != null ) {
                // A physical Java type has changed, and determine what the
                // corresponding local metadata identification string would have
                // been
                final JavaType typeName = PhysicalTypeIdentifier
                        .getJavaType(upstreamDependency);
                final LogicalPath typePath = PhysicalTypeIdentifier
                        .getPath(upstreamDependency);
                downstreamDependency = MaproModelFieldsMetadata.createIdentifier(
                        typeName, typePath);
            	doEvictAndGet(upstreamDependency,downstreamDependency);
            } else if (MemberFindingUtils.getAnnotationOfType(cid.getAnnotations(),
            		MaproModelJavaType.ROO_MAPRO_AUTOBEANS) != null ) {
            	// A physical Java type has changed, and determine what the
            	// corresponding local metadata identification string would have
            	// been
            	final JavaType typeName = PhysicalTypeIdentifier
            			.getJavaType(upstreamDependency);
            	final LogicalPath typePath = PhysicalTypeIdentifier
            			.getPath(upstreamDependency);
            	downstreamDependency = MaproModelFieldsMetadata.createIdentifier(
            			typeName, typePath);
            	doEvictAndGet(upstreamDependency,downstreamDependency);
            } else {
            	return;
            }

//            // We only need to proceed if the downstream dependency relationship
//            // is not already registered
//            // (if it's already registered, the event will be delivered directly
//            // later on)
//            if (metadataDependencyRegistry.getDownstream(upstreamDependency)
//                    .contains(downstreamDependency)) {
//                return;
//            }
        } else {
        	
        	doEvictAndGetDirect(downstreamDependency);
        }

    }

	private void doEvictAndGet(String upstreamDependency, String downstreamDependency) {
      // We only need to proceed if the downstream dependency relationship
      // is not already registered
      // (if it's already registered, the event will be delivered directly
      // later on)
      if (metadataDependencyRegistry.getDownstream(upstreamDependency)
              .contains(downstreamDependency)) {
          return;
      }		
		
		doEvictAndGetDirect(downstreamDependency);
	}

	private void doEvictAndGetDirect(String downstreamDependency) {
		// We should now have an instance-specific "downstream dependency" that
        // can be processed by this class
        Validate.isTrue(
                MetadataIdentificationUtils.getMetadataClass(
                        downstreamDependency).equals(
                        MetadataIdentificationUtils
                                .getMetadataClass(getProvidesType())),
                "Unexpected downstream notification for '"
                        + downstreamDependency
                        + "' to this provider (which uses '"
                        + getProvidesType() + "'");

        metadataService.evictAndGet(downstreamDependency);
	}
    
//	private String generateFields(JavaType name,
//			AnnotationMetadata mirrorAnnotation, 
//			final List<AnnotationMetadata> annotations
//	) {
//		final JCodeModel cm = new JCodeModel();
//		ModelGenerator modelGenerator = new ModelGenerator(cm);
//		
//		
//		
//		return new Generator(modelGenerator, cm, name, mirrorAnnotation,
//				annotations, null, null) {
//			protected void createTypeMap(java.util.List<JavaType> inputClasses) {}
//		}.generate();
////		return generateBeanMetadata(modelGenerator, cm, name, mirrorAnnotation,
////				annotations, null, null);
//		
//	}
	
//	private String generateInterfaces(JavaType name,
//			AnnotationMetadata mirrorAnnotation, 
//			final List<AnnotationMetadata> annotations
//	) {
//		final JCodeModel cm = new JCodeModel();
//		InterfaceGenerator modelGenerator = new InterfaceGenerator(cm);
//		
//		Generator generator = new Generator(modelGenerator, cm, name, mirrorAnnotation,
//				annotations) {
//			@Override
//			void createSetter(Map<String, PropertyInformationBean> map,
//					String propertyName, String setterName, JavaType returnType) {
//				if (typeMap.get(returnType)!=null) {
//					return;
//				} else {
//					super.createSetter(map, propertyName, setterName, returnType);
//				}
//			}
//			
//		};
//		generator.setDiscardAnnotation(MaproModelJavaType.ROO_MAPRO_NOINTERFACE);
//		generator.setCustomizer(new Function<ClassOrInterfaceTypeDetails, ClassCustomizer>() {
//
//			public ClassCustomizer apply(
//					final ClassOrInterfaceTypeDetails input) {
//				return new ClassCustomizer() {
//					public void customize(JDefinedClass definedClass) {
//						definedClass.annotate(cm.directClass(MaproModelJavaType.MODEL_INTERFACE.getFullyQualifiedTypeName()));
//					}
//				};
//			}
//		});
//
////		Map<JavaType, JavaType> typeMap = Maps.newHashMap();
////		
////		@SuppressWarnings({ "unchecked", "rawtypes" })
////		ArrayAttributeValue<ClassAttributeValue> classes = (ArrayAttributeValue) mirrorAnnotation.getAttribute("value");
////		for (final ClassAttributeValue c : classes.getValue()) {
////			final JavaType referredClass = c.getValue();
////			typeMap.put(referredClass,  new JavaType(referredClass.getSimpleTypeName(), name));
////		}
////		
////		generator.setTypeMap(typeMap);
//		
//		return generator.generate();
//		
//	}

	private String generateAutoBeans(JavaType name,
			AnnotationMetadata mirrorAnnotation, 
			final List<AnnotationMetadata> annotations
	) {
		final JCodeModel cm = new JCodeModel();
		AutoBeanGenerator modelGenerator = new AutoBeanGenerator(cm);
		
		Generator generator = new Generator(modelGenerator, cm, name, mirrorAnnotation,
				annotations) {

			@SuppressWarnings("rawtypes")
			@Override
			protected List<JavaType> getInputClasses() {
				List<JavaType> result = Lists.newArrayList();
				
				AnnotationAttributeValue value = mirrorAnnotation.getAttribute("value");
				Collection<String> packages;
				
				if (value instanceof StringAttributeValue) {
					StringAttributeValue stringValue = (StringAttributeValue) value;

					packages = Collections.singletonList(stringValue.getValue());
				} else {
					@SuppressWarnings("unchecked")
					ArrayAttributeValue<StringAttributeValue> arrayValue = (ArrayAttributeValue<StringAttributeValue>) value;

					packages = Lists.newArrayList();
					
					for (StringAttributeValue v : arrayValue.getValue()) {
						packages.add(v.getValue());
					}
				}
				
				for (String p : packages) {
					for (final Pom pom : projectOperations.getPoms()) {
						Collection<JavaType> typesForModule = typeLocationService
								.getTypesForModule(pom);
						for (final JavaType javaType : typesForModule) {
							
							String fullyQualifiedPackageName = javaType.getPackage().getFullyQualifiedPackageName();
							
							if (fullyQualifiedPackageName.equals(p)) {
								result.add(javaType);
							}
						}
					}
				}

				return result;
			}
			
			@Override
			protected List<PropertyInformation> getProperties(
					ClassOrInterfaceTypeDetails td) {
				List<PropertyInformation> props = Lists.newArrayList();
				
				List<? extends FieldMetadata> fields = td.getDeclaredFields();
				
				for (final FieldMetadata f : fields) {
					props.add(createPropertyInformation(f));
				}
				
				return props;
			}

			private PropertyInformation createPropertyInformation(
					final FieldMetadata f) {
				return new PropertyInformation() {
					
					public String getWriteMethod() {
						return BeanInfoUtils.getMutatorMethodName(f.getFieldName()).getSymbolName();
					}
					
					public JClass getType() {
						JavaType rt = getFieldType();
						return getJClass(rt) ;
					}

					private JavaType getFieldType() {
						JavaType rt = f.getFieldType();
						rt = getMappedType(rt);
						return rt;
					}
					
					public String getReadMethod() {
						return BeanInfoUtils.getAccessorMethodName(f.getFieldName(), getFieldType()).getSymbolName();
					}
					
					public String getName() {
						return f.getFieldName().getSymbolName();
					}

					public boolean isEnum() {
						ClassOrInterfaceTypeDetails typeDetails = typeLocationService.getTypeDetails(f.getFieldType());
						if (typeDetails!=null && typeDetails.getPhysicalTypeCategory()==PhysicalTypeCategory.ENUMERATION) {
							return true;
						}
						return false;
					}

					public PropertyInformation getInverseProperty() {
						// TODO handle inverse of ManyToOne
						// TODO create cache and return reference
						AnnotationMetadata annotation = f.getAnnotation(new JavaType("javax.persistence.OneToMany"));
						if (annotation==null) return null;
						@SuppressWarnings("rawtypes")
						final AnnotationAttributeValue mappedBy = annotation.getAttribute("mappedBy");
						if (mappedBy==null) return null;
						return new PropertyInformation() {
							
							public boolean isEnum() {
								return false;
							}
							
							public String getWriteMethod() {
								return null;
							}
							
							public JClass getType() {
								return null;
							}
							
							public String getReadMethod() {
								return null;
							}
							
							public String getName() {
								return ((StringAttributeValue)mappedBy).getValue();
							}
							
							public PropertyInformation getInverseProperty() {
								return null;
							}
						};
					}
				};
			}
			
			@Override
			protected void createTypeMap(List<JavaType> inputClasses) {
			}
			
		};

//		Map<JavaType, JavaType> typeMap = Maps.newHashMap();
//		
//		@SuppressWarnings({ "unchecked", "rawtypes" })
//		ArrayAttributeValue<ClassAttributeValue> classes = (ArrayAttributeValue) mirrorAnnotation.getAttribute("value");
//		for (final ClassAttributeValue c : classes.getValue()) {
//			final JavaType referredClass = c.getValue();
//			typeMap.put(referredClass,  new JavaType(referredClass.getSimpleTypeName(), name));
//		}
//		
//		generator.setTypeMap(typeMap);
		
		return generator.generate();
		
	}
	
//	private String generateProxies(
//			JavaType name,
//			AnnotationMetadata mirrorAnnotation, 
//			final List<AnnotationMetadata> annotations
//	) {
//		final JCodeModel cm = new JCodeModel();
//		InterfaceGenerator modelGenerator = new InterfaceGenerator(cm);
//		
////		Map<JavaType, JavaType> typeMap = Maps.newHashMap();
////		
////		@SuppressWarnings({ "unchecked", "rawtypes" })
////		ArrayAttributeValue<ClassAttributeValue> classes = (ArrayAttributeValue) mirrorAnnotation.getAttribute("value");
////		for (final ClassAttributeValue c : classes.getValue()) {
////			final JavaType referredClass = c.getValue();
////			typeMap.put(referredClass,  new JavaType(referredClass.getSimpleTypeName(), name));
////		}
//		
//		return new Generator(modelGenerator, cm, name, mirrorAnnotation,
//				annotations, MaproModelJavaType.ROO_MAPRO_NOPROXY,
//				new Function<ClassOrInterfaceTypeDetails, ClassCustomizer>() {
//
//					public ClassCustomizer apply(
//							final ClassOrInterfaceTypeDetails input) {
//						return new ClassCustomizer() {
//							public void customize(JDefinedClass definedClass) {
//								
//								JAnnotationUse pf = definedClass.annotate(cm.directClass(GwtJavaType.PROXY_FOR.getFullyQualifiedTypeName()));
//								pf.param("value", cm.directClass(input.getName().getFullyQualifiedTypeName()));
//								pf.param("locator", cm.directClass(MaproModelJavaType.LONGID_LOCATOR.getFullyQualifiedTypeName()));
//
//								definedClass._implements(cm.directClass(GwtJavaType.ENTITY_PROXY.getFullyQualifiedTypeName()));
//
//								for (JavaType im : input.getImplementsTypes()) {
//									ClassOrInterfaceTypeDetails typeDetails = getTypeDetails(im);
//									if (typeDetails!=null && MemberFindingUtils.getAnnotationOfType(typeDetails.getAnnotations(), MaproModelJavaType.MODEL_INTERFACE)!=null) {
//										definedClass._implements(cm.directClass(im.getFullyQualifiedTypeName()));
//									}
//								}
//								
//							}
//						};
//					}
//				}
//		).generate();
//		
//	}
	
//	private String generateBeanMetadata(
//			BeanMetadataGenerator modelGenerator,
//			final JCodeModel cm, 
//			JavaType name,
//			AnnotationMetadata mirrorAnnotation,
//			final List<AnnotationMetadata> annotations, 
//			JavaType discardAnnotation,
//			final Function<ClassOrInterfaceTypeDetails, 
//			ClassCustomizer> customizer
//	) {
//		return new Generator(modelGenerator, cm, name, mirrorAnnotation, annotations, discardAnnotation, customizer).generate();
//	}

	protected boolean isNoInterface(MethodMetadata m0,
			List<MethodMetadata> noInterface) {
		List<JavaType> types = AnnotatedJavaType.convertFromAnnotatedJavaTypes(m0.getParameterTypes());
		
		for (MethodMetadata m : noInterface) {
			if (
					m.getMethodName().equals(m0.getMethodName())
					&&
					AnnotatedJavaType.convertFromAnnotatedJavaTypes(m.getParameterTypes()).equals(types)
			) {
				return true;
			}
		}
		
		return false;
	}

	private List<MethodMetadata> getNoInterfaceMethods(
			ClassOrInterfaceTypeDetails td, JavaType discardAnnotation) {
		List<MethodMetadata> result = Lists.newArrayList();
		
		if (discardAnnotation!=null) {
			getNoInterfaceMethods(td, result, discardAnnotation);
		}
		
		return result;
	}

	private void getNoInterfaceMethods(ClassOrInterfaceTypeDetails td,
			List<MethodMetadata> result, JavaType discardAnnotation) {
		if (td==null) return;
		
		
		if (MemberFindingUtils
		        .getAnnotationOfType(td.getAnnotations(),
		                discardAnnotation)!=null) {
			result.addAll(td.getDeclaredMethods());
		} else {
			for (MethodMetadata m : td.getDeclaredMethods()) {
				if (MemberFindingUtils
			        .getAnnotationOfType(m.getAnnotations(),
			                discardAnnotation)!=null) {
					result.add(m);
				}
				
			}
		}
		
		for (JavaType t : td.getExtendsTypes()) {
			getNoInterfaceMethods(typeLocationService.getTypeDetails(t), result, discardAnnotation);
		}
		
		for (JavaType t : td.getImplementsTypes()) {
			getNoInterfaceMethods(typeLocationService.getTypeDetails(t), result, discardAnnotation);
		}
	}


	private ClassOrInterfaceTypeDetails getTypeDetails(
			final JavaType referredClass) {
		JavaType enclosing = referredClass.getEnclosingType();
		
		if (enclosing==null) {
			return typeLocationService.getTypeDetails(referredClass);
		} else {
			ClassOrInterfaceTypeDetails td = getTypeDetails(enclosing);
			
			return td.getDeclaredInnerType(referredClass);
		}
		
	}
    
	private static String getTypeName(JavaType rt) {
		String typeName = rt.getNameIncludingTypeParameters();
		
		if (rt.isPrimitive() && !rt.isArray()) {
			try {
				for (Class<?> p : Primitives.allPrimitiveTypes()) {
					if (p.getSimpleName().equals(typeName)) {
						typeName = Primitives.wrap(p).getCanonicalName();
						break;
					}
				}
				
				typeName = Primitives.wrap(Class.forName(typeName)).getCanonicalName();
			} catch (ClassNotFoundException e) {
				throw new RuntimeException(e);
			}
		}
		return typeName;
	}

	private static PropertyInformationBean getInfo(Map<String, PropertyInformationBean> map, String propertyName) {
		PropertyInformationBean b = map.get(propertyName);
		
		if (b==null) {
			b = new PropertyInformationBean();
			b.setName(StringUtils.uncapitalize(propertyName));
			map.put(propertyName, b);
		}
		
		return b;
	}
	
    public static boolean isAccessorMethod(final MethodMetadata method, boolean isInterface) {
        Validate.notNull(method, "Method is required");
        return (method.getMethodName().getSymbolName().startsWith("get") || method
                .getMethodName().getSymbolName().startsWith("is"))
                && method.getParameterTypes().isEmpty()
                && (isInterface || Modifier.isPublic(method.getModifier()));
    }

    public static boolean isMutatorMethod(final MethodMetadata method, boolean isInterface) {
        Validate.notNull(method, "Method is required");
        return method.getMethodName().getSymbolName().startsWith("set")
                && method.getParameterTypes().size() == 1
                && (isInterface || Modifier.isPublic(method.getModifier()));
    }

	class Generator {
		
		BeanMetadataGenerator modelGenerator;
		JCodeModel cm;
		JavaType name;
		AnnotationMetadata mirrorAnnotation;
		List<AnnotationMetadata> annotations; 
		JavaType discardAnnotation;
		Function<ClassOrInterfaceTypeDetails, ClassCustomizer> customizer;
		Map<JavaType, JavaType> typeMap;
		
		public Generator(BeanMetadataGenerator modelGenerator, JCodeModel cm,
				JavaType name, AnnotationMetadata mirrorAnnotation,
				List<AnnotationMetadata> annotations) {
			super();
			this.modelGenerator = modelGenerator;
			this.cm = cm;
			this.name = name;
			this.mirrorAnnotation = mirrorAnnotation;
			this.annotations = annotations;
		}

		public Generator(
				BeanMetadataGenerator modelGenerator,
				JCodeModel cm,
				JavaType name,
				AnnotationMetadata mirrorAnnotation,
				List<AnnotationMetadata> annotations,
				JavaType discardAnnotation,
				Function<ClassOrInterfaceTypeDetails, ClassCustomizer> customizer
				//Map<JavaType, JavaType> typeMap
		) {
			super();
			this.modelGenerator = modelGenerator;
			this.cm = cm;
			this.name = name;
			this.mirrorAnnotation = mirrorAnnotation;
			this.annotations = annotations;
			this.discardAnnotation = discardAnnotation;
			this.customizer = customizer;
			//this.typeMap = typeMap;
		}

		class Info implements Function<JavaType, TypeInfo<JavaType>> {

			public TypeInfo<JavaType> apply(final JavaType referredClass) {
				//final boolean isBuiltin = MetaUtils.isBuiltinType(referredClass.getFullyQualifiedTypeName());
				
				final ClassOrInterfaceTypeDetails td = getTypeDetails(referredClass);
				final MemberDetails md = memberDetailsScanner.getMemberDetails(MaproModelFieldsMetadataProviderImpl.class.getName(), td);
				
				return new TypeInfo<JavaType>() {

					public boolean isEnumeration() {
						return td.getPhysicalTypeCategory()==PhysicalTypeCategory.ENUMERATION;
					}

					public boolean isEntity() {
						return md.getAnnotation(new JavaType("javax.persistence.Entity"))!=null;
					}

					public String getFullName() {
						return referredClass.getFullyQualifiedTypeName();
					}

					public JavaType getSuperType() {
						if (td.getSuperclass()==null || td.getSuperclass().equals(JavaType.OBJECT)) return null;
						return td.getSuperclass().getName();
					}

					public Collection<FieldInfo<JavaType>> getFields() {
						
						List<PropertyInformation> props = getProperties(td);
						
						return Collections2.transform(props, new Function<PropertyInformation, FieldInfo<JavaType>>() {

							public FieldInfo<JavaType> apply(
									final PropertyInformation prop) {
								return new FieldInfo<JavaType>() {

									public String getName() {
										return prop.getName();
									}

									public String getReadMethod() {
										return prop.getReadMethod();
									}

									public String getWriteMethod() {
										return prop.getWriteMethod();
									}

									public boolean isList() {
										return prop.getType().erasure().fullName().equals(List.class.getName());
									}

									public boolean isSet() {
										return prop.getType().erasure().fullName().equals(Set.class.getName());
									}

									public String getInverseFieldName() {
										if (prop.getInverseProperty()==null) return null;
										return prop.getInverseProperty().getName();
									}

									public JavaType getValueType() {
										if (isList()||isSet()) {
											return new JavaType(prop.getType().getTypeParameters().get(0).fullName());
										} else {
											return new JavaType(prop.getType().fullName());
										}
									}
								};
							}
							
						});						
					}

					public ClassCustomizer getCustomizer() {
						if (customizer==null) return null;
						return customizer.apply(td);
					}

					public boolean isAbstract() {
						return td.isAbstract();
					}
				};
			}
			
		}
		
		String generate() {
			MetaFactory<JavaType> mf = new MetaFactory<JavaType>(getInputClasses(), new Info());
			
			
			
			ByteArrayOutputStream os = new ByteArrayOutputStream();
			
			List<JavaType> inputClasses = getInputClasses();
			
			
			createTypeMap(inputClasses);
			
			BeanInformation[] bis = new BeanInformation[inputClasses.size()];
			
			int idx = 0;
			for (final JavaType referredClass : inputClasses) {
				
				final ClassOrInterfaceTypeDetails td = getTypeDetails(referredClass);
				final MemberDetails md = memberDetailsScanner.getMemberDetails(MaproModelFieldsMetadataProviderImpl.class.getName(), td);
				
				bis[idx++] = new BeanInformation() {
					
					public List<PropertyInformation> getProperties() {
						return Generator.this.getProperties(td);
					}
					

					JClass beanClass = new JClass(cm) {

						@Override
						public String name() {
							return referredClass.getSimpleTypeName();							
						}

						@Override
						public JPackage _package() {
							return cm._package(referredClass.getPackage().getFullyQualifiedPackageName());
						}

						@Override
						public JClass _extends() {
							ClassOrInterfaceTypeDetails sc = td.getSuperclass();
							if (sc==null) return cm.ref(Object.class);
							return cm.directClass(sc.getName().getFullyQualifiedTypeName());
						}

						@Override
						public Iterator<JClass> _implements() {
							return Iterators.emptyIterator();
						}

						@Override
						public boolean isInterface() {
							return false;
						}

						@Override
						public boolean isAbstract() {
							return false;
						}

						@Override
						protected JClass substituteParams(JTypeVar[] variables,
								List<JClass> bindings) {
							return null;
						}

						@Override
						public String fullName() {
							return referredClass.getFullyQualifiedTypeName();
						}
						
					};
					public JClass getBeanClass() {
						return beanClass;
					}

					public ClassCustomizer getCustomizer() {
						if (customizer==null) return null;
						return customizer.apply(td);
					}

					public boolean isEntity() {
						return md.getAnnotation(new JavaType("javax.persistence.Entity"))!=null;
					}

				};
			}
			
			@SuppressWarnings("rawtypes")
			AnnotationAttributeValue serviceClassAnnotation = mirrorAnnotation.getAttribute("service");
			JClass serviceClass = null;
			if (serviceClassAnnotation!=null) {
				ClassAttributeValue cav = (ClassAttributeValue)serviceClassAnnotation;
				serviceClass = cm.ref(cav.getValue().getFullyQualifiedTypeName());
			}
			
			modelGenerator.generateFields(
					os,
					name.getPackage().getFullyQualifiedPackageName(),
					name.getSimpleTypeName(),
					new Annotater() {
						public void annotate(JAnnotatable dc) {
							// TODO full copy
							for (AnnotationMetadata am : annotations) {
								JAnnotationUse au = dc.annotate(cm.directClass(am.getAnnotationType().getFullyQualifiedTypeName()));
								
								forAttributes(am, au);
								
							}
						}
						
						private void forAttributes(AnnotationMetadata am,
								JAnnotationUse au) {
							for (JavaSymbolName an : am.getAttributeNames()) {
								AnnotationAttributeValue<?> att = am.getAttribute(an);
								
								forAttribute(au, an, att);
							}
						}
						
						@SuppressWarnings({ "rawtypes", "unchecked" })
						private void forAttribute(JAnnotationUse au,
								JavaSymbolName an, AnnotationAttributeValue<?> att) {
							if (att instanceof ArrayAttributeValue) {
								ArrayAttributeValue<AnnotationAttributeValue<?>> avArray = (ArrayAttributeValue) att;
								
								JAnnotationArrayMember pa = au.paramArray(an.getSymbolName());
								
								for (AnnotationAttributeValue<?> aav : avArray.getValue()) {
									
									if (aav instanceof ClassAttributeValue) {
										ClassAttributeValue cav = (ClassAttributeValue) aav;
										
										pa.param(cm.directClass(cav.getValue().getFullyQualifiedTypeName()));
									} else if (aav instanceof StringAttributeValue) {
										StringAttributeValue sav = (StringAttributeValue) aav;
										
										pa.param(sav.getValue());
									}
									
								}
								
							} else if (att instanceof StringAttributeValue) {
								StringAttributeValue avArray = (StringAttributeValue) att;
								
								au.param(an.getSymbolName(), avArray.getValue());
								
								
							} else if (att instanceof ClassAttributeValue) {
								ClassAttributeValue avArray = (ClassAttributeValue) att;
								
								au.param(an.getSymbolName(), cm.directClass(avArray.getValue().getFullyQualifiedTypeName()));
							}
						}
					},
					serviceClass,
					mf.generate(),
					mf.extraInfo
			);
			
			return new String(os.toByteArray());
		}

		protected void createTypeMap(List<JavaType> inputClasses) {
			typeMap = Maps.newHashMap();
			for (final JavaType referredClass : inputClasses) {
				typeMap.put(referredClass,  new JavaType(referredClass.getSimpleTypeName(), name));
			}
		}

		protected List<JavaType> getInputClasses() {
			List<JavaType> result = Lists.newArrayList();
			
			@SuppressWarnings({ "unchecked", "rawtypes" })
			ArrayAttributeValue<ClassAttributeValue> classes = (ArrayAttributeValue) mirrorAnnotation.getAttribute("value");
			
			for (final ClassAttributeValue c : classes.getValue()) {
				result.add(c.getValue());
			}
			
			return result;
		}

		public void setDiscardAnnotation(JavaType discardAnnotation) {
			this.discardAnnotation = discardAnnotation;
		}

		public void setCustomizer(
				Function<ClassOrInterfaceTypeDetails, ClassCustomizer> customizer) {
			this.customizer = customizer;
		}

//		public void setTypeMap(Map<JavaType, JavaType> typeMap) {
//			this.typeMap = typeMap;
//		}

		void createSetter(Map<String, PropertyInformationBean> map,
				MethodMetadata m) {
			String propertyName = BeanInfoUtils.getPropertyNameForJavaBeanMethod(m).getSymbolName();
			String setterName = m.getMethodName().getSymbolName();
			JavaType returnType = AnnotatedJavaType.convertFromAnnotatedJavaType(m.getParameterTypes().get(0));
			createSetter(map, propertyName, setterName, returnType);
		}

		void createSetter(Map<String, PropertyInformationBean> map,
				String propertyName, String setterName, JavaType returnType) {
			PropertyInformationBean b = getInfo(map, propertyName);
			returnType = getMappedType(returnType);
			String typeName = getTypeName(returnType);
			b.setWriteMethod(setterName);
			b.setType(cm.directClass(typeName));
		}

		protected List<PropertyInformation> getProperties(
				final ClassOrInterfaceTypeDetails td) {
			final MemberDetails memberDetails = memberDetailsScanner
					.getMemberDetails(MaproModelFieldsMetadataProviderImpl.class.getName(),
							td);
			
			final boolean isInterface = td.getPhysicalTypeCategory()==PhysicalTypeCategory.INTERFACE; 
			final List<MethodMetadata> noInterface = getNoInterfaceMethods(td, discardAnnotation);
			
			Map<String, PropertyInformationBean> map = Maps.newHashMap();
			
			for(MethodMetadata m : memberDetails.getMethods()) {
				if (isNoInterface(m, noInterface)) {
					continue;
				}
				
				if (isAccessorMethod(m, isInterface)) {
					PropertyInformationBean b = getInfo(map, BeanInfoUtils.getPropertyNameForJavaBeanMethod(m).getSymbolName());
					b.setReadMethod(m.getMethodName().getSymbolName());
					JavaType rt = m.getReturnType();
					rt = getMappedType(rt);
					String typeName = getTypeName(rt);
					b.setType(cm.directClass(typeName));
				} else if (isMutatorMethod(m, isInterface)) {
					createSetter(map, m);
				}
			}
			
			return Lists.<PropertyInformation>newArrayList(map.values());
		}
	
		protected JavaType getMappedType(JavaType rt) {
			if (typeMap!=null) {
				if (rt.getFullyQualifiedTypeName().equals(List.class.getName())) {
					JavaType et = getMappedType(rt.getParameters().get(0));
					rt = new JavaType(List.class.getName(), 0, DataType.TYPE, null, Collections.singletonList(et));
				} else if (rt.getFullyQualifiedTypeName().equals(Set.class.getName())) {
					JavaType et = getMappedType(rt.getParameters().get(0));
					rt = new JavaType(Set.class.getName(), 0, DataType.TYPE, null, Collections.singletonList(et));
				} else if (rt.getFullyQualifiedTypeName().equals(Map.class.getName())) {
					JavaType et1 = getMappedType(rt.getParameters().get(0));
					JavaType et2 = getMappedType(rt.getParameters().get(0));
					rt = new JavaType(Map.class.getName(), 0, DataType.TYPE, null, Arrays.asList(et1, et2));
				} else {
					JavaType rt2 = typeMap.get(rt);
					if (rt2!=null) {
						rt = getMappedType(rt2);
					}
				}
			}
			
			return rt;
		}
		
		protected JClass getJClass(JavaType javaType) {
			if (javaType.isPrimitive() && !javaType.isArray()) {
				return JType.parse(cm, javaType.getFullyQualifiedTypeName()).boxify();
			}
			
			List<JavaType> parameters = javaType.getParameters();
			if (parameters!=null && !parameters.isEmpty()) {
				JClass baseType = cm.directClass(javaType.getFullyQualifiedTypeName());
				List<JClass> params = Lists.newArrayList();
				for (JavaType p : parameters) {
					params.add((JClass)getJClass(p));
				}
				return baseType.narrow(params);
			}
			
			return cm.directClass(javaType.getFullyQualifiedTypeName());
		}

		
	}
	
}
